package com.pinfly.common.crypto;

import java.io.IOException;
import java.math.BigInteger;

import com.pinfly.common.util.HexConvert;

/**
 * Represents a Microsoft RSA Public key as obtained by Microsoft's Base or
 * Enhanced RSA Crypto provider. The constructors in this class assume that the
 * keyData that is being passed has the following C structure, as defined by
 * Microsoft: <CODE><PRE>
 * PUBLICKEYSTRUC publickeystruc;
 * RSAPUBKEY rsapubkey;
 * BYTE modulus[rsapubkey.bitlen/8];
 * 
 * Where PUBLICKEYSTRUC is:
 * 
 *   typedef struct _PUBLICKEYSTRUC {
 *     BYTE bType;
 *     BYTE bVersion;
 *     WORD reserved;
 *     ALG_ID aiKeyAlg;
 *   }   BLOBHEADER, PUBLICKEYSTRUC
 * 
 * Where ALG_ID is:
 *   typedef unsigned int ALG_ID;
 * 
 * Where RSAPUBKEY is:
 *   typedef struct _RSAPUBKEY {
 *     DWORD magic;
 *     DWORD bitlen;
 *     DWORD pubexp;
 *   } RSAPUBKEY
 * </PRE></CODE> For more information regarding this structure see the following
 * documents:<BR/>
 * <A href=
 * "http://msdn.microsoft.com/library/default.asp?url=/library/en-us/security/security/enhanced_provider_key_blobs.asp"
 * > Microsoft Base and Enhanced Cryptographic Service Providers</A> <BR/>
 * <A href=
 * "http://msdn.microsoft.com/library/default.asp?url=/library/en-us/security/security/cryptography_structures.asp"
 * > Microsoft Cryptography Structures</A>
 * 
 */
public class MSRSAPublicKey
{
    // Possible values for magic
    private static final int RSA1 = 0x31415352;
    private static final int RSA2 = 0x32415352;

    /**
     * The default type of key, which is PUBLICKEYBLOB
     */
    public static final byte DEFAULT_TYPE = 6; // PUBLICKEYBLOB

    /**
     * The default version.&nbsp;Note: The Microsoft documentation states that
     * the version field must always have this default value.
     */
    public static final byte DEFAULT_VERSION = 0x02; // Always

    /**
     * The default reservered value.&nbsp;Note: The Microsoft documentation
     * states that the reserved field must always have this default value.
     */
    public static final short DEFAULT_RESERVED = 0x00; // Always

    /**
     * The default algorithm identifier.&nbsp;This is CALG_RSA_SIGN.
     */
    public static final int DEFAULT_ALGID = 9216; // CALG_RSA_SIGN

    /**
     * All public keys should have a magic set to this value (RSA1)
     */
    public static final int DEFAULT_MAGIC = RSA1; // RSA1 - Publickeys

    private static final int DEFAULT_BITLEN = 512;

    // PUBLICKKEYSTRUCT
    private byte m_type = DEFAULT_TYPE;
    private byte m_version = DEFAULT_VERSION;
    private short m_reserved = DEFAULT_RESERVED; // WORD
    private int m_algId = DEFAULT_ALGID; // unsigned int

    // RSAPUBKEY
    private int m_magic = DEFAULT_MAGIC; // DWORD
    private int m_bitLen = DEFAULT_BITLEN; // DWORD

    private int m_exponent; // DWORD

    // Modulus
    private BigInteger m_modulus = BigInteger.valueOf (0);

    /**
     * Creates a new instance. Values for type, version, reserved, algId, and
     * magic are set to their default values, which are defined by the
     * DEFAULT_xx static variables of this class.
     */
    public MSRSAPublicKey ()
    {
    }

    /**
     * Creates a new instance given a Microsoft RSA public key in hex.
     * 
     * @param keyData The Microsoft key as a hex string
     * @throws IOException If the keyData is not in the proper format.
     */
    public MSRSAPublicKey (String keyData) throws IOException
    {
        this (HexConvert.convertToBytes (keyData));
    }

    /**
     * Creates a new instance given a Microsoft RSA public key.
     * 
     * @param keyData A byte array that represents the Microsoft key.
     * @throws IOException If the keyData is not in the proper format.
     */
    public MSRSAPublicKey (byte[] keyData) throws IOException
    {
        load (keyData);
    }

    /**
     * Creates a new instance from an X509RSAPublicKey. Values for type,
     * version, reserved, algId, and magic are set to their default values,
     * which are defined by the DEFAULT_xx static variables of this class.
     * 
     * @param key The X509RSAPublicKey
     */
    public MSRSAPublicKey (X509RSAPublicKey key)
    {
        setExponent (key.getExponent ());
        setModulus (key.getModulus ());
    }

    private void load (byte[] keyData) throws IOException
    {
        KeyInputStream stream = null;

        try
        {
            stream = new KeyInputStream (keyData, true);
            m_type = stream.readByte ();
            m_version = stream.readByte ();
            m_reserved = stream.readShort ();
            m_algId = stream.readInt ();
            m_magic = stream.readInt ();
            m_bitLen = stream.readInt ();
            m_exponent = stream.readInt ();

            byte[] mod = stream.read (m_bitLen / 8);

            // The modulus is always positive, but MS does not use the
            // 2's complement sign bit. Need to add it here prior to
            // creating a BigInteger.
            mod = KeyUtil.prependToArray ((byte) 0, mod);

            m_modulus = new BigInteger (mod);
        }
        finally
        {
            try
            {
                if (stream != null)
                {
                    stream.close ();
                }
            }
            catch (IOException e)
            {
            }
        }

    }

    /**
     * Gets the type
     * 
     * @return the type
     */
    public byte getType ()
    {
        return m_type;
    }

    /**
     * Sets the type
     * 
     * @param type The type
     */
    public void setType (byte type)
    {
        m_type = type;
    }

    /**
     * Gets the version
     * 
     * @return the version
     */
    public byte getVersion ()
    {
        return m_version;
    }

    /**
     * Sets the version
     * 
     * @param ver The version
     */
    public void setVersion (byte ver)
    {
        m_version = ver;
    }

    /**
     * Gets the reserved contents
     * 
     * @return The reserved contents
     */
    public short getReserved ()
    {
        return m_reserved;
    }

    /**
     * Sets the reserved contents
     * 
     * @param res The reserved contents
     */
    public void setReserved (short res)
    {
        m_reserved = res;
    }

    /**
     * Gets the Algorithm id
     * 
     * @return The algorithm id
     */
    public int getAlgId ()
    {
        return m_algId;
    }

    /**
     * Sets the Algorithm id
     * 
     * @param algId The algorithm id
     */
    public void setAlgId (int algId)
    {
        m_algId = algId;
    }

    /**
     * Gets the magic
     * 
     * @return The magic
     */
    public int getMagic ()
    {
        return m_magic;
    }

    /**
     * Sets the magic
     * 
     * @param mag The magic
     */
    public void setMagic (int mag)
    {
        m_magic = mag;
    }

    /**
     * Gets the length of the modulus
     * 
     * @return The modulus length in bit's
     */
    public int getBitLen ()
    {
        return (m_bitLen);
    }

    /**
     * Sets the length of the modulus
     * 
     * @param len The len in bits
     */
    public void setBitLen (int len)
    {
        m_bitLen = len;
    }

    /**
     * Gets the exponent
     * 
     * @return The RSA exponent
     */
    public int getExponent ()
    {
        return m_exponent;
    }

    /**
     * Sets the exponent
     * 
     * @param exp The RSA exponent
     */
    public void setExponent (int exp)
    {
        m_exponent = exp;
    }

    /**
     * Sets the exponent
     * 
     * @param in The RSA exponent
     */
    public void setExponent (BigInteger in)
    {
        setExponent (in.intValue ());
    }

    /**
     * Gets the modulus
     * 
     * @return the RSA modulus.
     */
    public BigInteger getModulus ()
    {
        return m_modulus;
    }

    /**
     * Sets the modulus
     * 
     * @param mod The RSA modulus
     */
    public void setModulus (BigInteger mod)
    {
        m_modulus = mod;

        // Re-calculate bit length because the modulus changed

        // Get rid of initial positive sign byte. Per the PKCS standard,
        // the modulus is always positive, and Microsoft does not
        // expect the extra 0 sign byte that starts the 2's
        // complement of a positive BigInteger.
        byte[] modbytes = KeyUtil.convertToBytes (m_modulus);

        m_bitLen = modbytes.length * 8;
    }

    /**
     * Gets this public RSA key as a byte array
     * 
     * @return The byte array that represents this key.
     */
    public byte[] getEncoded ()
    {
        // Convert any values back into a key blob
        KeyOutputStream stream = null;

        try
        {
            // Get rid of initial positive sign byte. Per the PKCS standard,
            // the modulus is always positive, and Microsoft does not
            // expect the extra 00 sign byte that starts the 2's
            // complement of a positive BigInteger.
            byte[] modbytes = KeyUtil.convertToBytes (m_modulus);

            stream = new KeyOutputStream (true);
            stream.write (m_type);
            stream.write (m_version);
            stream.write (m_reserved);
            stream.write (m_algId);
            stream.write (m_magic);
            stream.write (m_bitLen);
            stream.write (m_exponent);
            stream.write (modbytes);
            return stream.toByteArray ();
        }
        finally
        {
            try
            {
                if (stream != null)
                {
                    stream.close ();
                }
            }
            catch (IOException ioe)
            {
            }
        }
    }

    /**
     * Gets this public RSA key as a hex string
     * 
     * @return The hex representation of this key
     */
    public String getEncodedAsString ()
    {
        return (HexConvert.convertHexString (getEncoded ()));
    }

    public String toString ()
    {
        return "*** MSRSAPublicKey ***" + "\ntype: " + m_type + "\nversion: " + m_version + "\nreserved: " + m_reserved
               + "\nalgId: " + m_algId + "\nmagic: " + m_magic + "\nbitlen: " + m_bitLen + "\npubexp: " + m_exponent
               + "\nmodulus: " + m_modulus;
    }

}
